//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Foundation

import XCTest
import AWSCognitoIdentityProvider
import Amplify
@testable import AWSCognitoAuthPlugin
@testable import AWSPluginsTestCommon
import ClientRuntime

class DeviceBehaviorFetchDevicesTests: BasePluginTest {

    override func setUp() {
        super.setUp()
        mockIdentityProvider = MockIdentityProvider(
            mockListDevicesOutput: { _ in
                try ListDevicesOutput()
            }
        )
    }

    /// Test fetchDevices operation can be invoked
    ///
    /// - Given: Given a configured auth plugin
    /// - When:
    ///    - I call fetchDevices operation
    /// - Then:
    ///    - I should get a successful task execution
    ///
    func testFetchDevicesRequest() async throws {
        mockIdentityProvider = MockIdentityProvider(
            mockListDevicesOutput: { _ in
                ListDevicesOutput(devices: [CognitoIdentityProviderClientTypes.DeviceType(deviceKey: "id")], paginationToken: nil)
            }
        )
        let options = AuthFetchDevicesRequest.Options()
        _ = try await plugin.fetchDevices(options: options)
    }

    /// Test fetchDevices operation can be invoked without options
    ///
    /// - Given: Given a configured auth plugin
    /// - When:
    ///    - I call fetchDevices operation
    /// - Then:
    ///    - I should get a successful task execution
    ///
    func testFetchDevicesRequestWithoutOptions() async throws {
        mockIdentityProvider = MockIdentityProvider(
            mockListDevicesOutput: { _ in
                ListDevicesOutput(devices: [CognitoIdentityProviderClientTypes.DeviceType(deviceKey: "id")], paginationToken: nil)
            }
        )
        _ = try await plugin.fetchDevices()
    }

    /// Test a successful fetchDevices call
    ///
    /// - Given: an auth plugin with mocked service. Mocked service calls should mock a successful response
    /// - When:
    ///    - I invoke fetchDevices
    /// - Then:
    ///    - I should get a successful result with one device fetched
    ///
    func testSuccessfulListDevices() async throws {
        let dateToTest = Date()
        let deviceName = "test device"
        let deviceId = "deviceId"
        mockIdentityProvider = MockIdentityProvider(
            mockListDevicesOutput: { _ in
                ListDevicesOutput(
                    devices: [
                        CognitoIdentityProviderClientTypes.DeviceType(
                            deviceAttributes: [
                                .init(name: "device_name", value: deviceName)
                            ],
                            deviceCreateDate: dateToTest,
                            deviceKey: deviceId,
                            deviceLastAuthenticatedDate: dateToTest,
                            deviceLastModifiedDate: dateToTest
                        )
                    ], paginationToken: nil)
            }
        )
        let listDevicesResult = try await plugin.fetchDevices()
        guard listDevicesResult.count == 1 else {
            XCTFail("Result should have device count of 1")
            return
        }
        guard let awsAuthDevice = listDevicesResult.first as? AWSAuthDevice else {
            XCTFail("Resultant device type should be AWSAuthDevice")
            return
        }
        XCTAssertEqual(awsAuthDevice.name, deviceName)
        XCTAssertEqual(awsAuthDevice.id, deviceId)
        XCTAssertNotEqual(awsAuthDevice.attributes?.count, 0)
        XCTAssertEqual(awsAuthDevice.createdDate, dateToTest)
        XCTAssertEqual(awsAuthDevice.lastAuthenticatedDate, dateToTest)
        XCTAssertEqual(awsAuthDevice.lastModifiedDate, dateToTest)
    }

    /// Test a fetchDevices call with invalid response from service
    ///
    /// - Given: an auth plugin with mocked service. Mocked service calls should mock a invalid response
    /// - When:
    ///    - I invoke fetchDevices
    /// - Then:
    ///    - I should get a .unknown error
    ///
    func testListDevicesWithInvalidResult() async throws {

        mockIdentityProvider = MockIdentityProvider(
            mockListDevicesOutput: { _ in
                ListDevicesOutput(devices: nil, paginationToken: nil)
            }
        )
        do {
            let listDevicesResult = try await plugin.fetchDevices()
            XCTFail("Should not receive a success response \(listDevicesResult)")
        } catch {
            guard case AuthError.unknown = error else {
                XCTFail("Should receive unknown error instead got \(error)")
                return
            }
        }
    }

    // MARK: - Service error for listDevices

    /// Test a fetchDevices with `InternalErrorException` from service
    ///
    /// - Given: Given an auth plugin with mocked service. Mocked service should mock a
    ///   InternalErrorException response for fetchDevice
    ///
    /// - When:
    ///    - I invoke fetchDevices
    /// - Then:
    ///    - I should get a .unknown error
    ///
    func testListDevicesWithInternalErrorException() async throws {

        mockIdentityProvider = MockIdentityProvider(
            mockListDevicesOutput: { _ in
                throw AWSCognitoIdentityProvider.InternalErrorException()
            }
        )
        do {
            let listDevicesResult = try await plugin.fetchDevices()
            XCTFail("Should not receive a success response \(listDevicesResult)")
        } catch {
            guard case AuthError.unknown = error else {
                XCTFail("Should produce unknown error")
                return
            }
        }
    }

    /// Test a fetchDevices call with InvalidParameterException response from service
    ///
    /// - Given: an auth plugin with mocked service. Mocked service should mock a
    ///   InvalidParameterException response
    ///
    /// - When:
    ///    - I invoke fetchDevices
    /// - Then:
    ///    - I should get a .service error with  .invalidParameter as underlyingError
    ///
    func testListDevicesWithInvalidParameterException() async throws {

        mockIdentityProvider = MockIdentityProvider(
            mockListDevicesOutput: { _ in
                throw AWSCognitoIdentityProvider.InvalidParameterException(
                    message: "invalid parameter"
                )
            }
        )
        do {
            _ = try await plugin.fetchDevices()
            XCTFail("Should return an error if the result from service is invalid")
        } catch {
            guard case AuthError.service(_, _, let underlyingError) = error else {
                XCTFail("Should produce service error instead of \(error)")
                return
            }
            guard case .invalidParameter = (underlyingError as? AWSCognitoAuthError) else {
                XCTFail("Underlying error should be invalidParameter \(error)")
                return
            }
        }
    }

    /// Test a fetchDevices call with InvalidParameterException response from service
    ///
    /// - Given: an auth plugin with mocked service. Mocked service should mock a
    ///   InvalidUserPoolConfigurationException response
    ///
    /// - When:
    ///    - I invoke fetchDevices
    /// - Then:
    ///    - I should get a .configuration error
    ///
    func testListDevicesWithInvalidUserPoolConfigurationException() async throws {

        mockIdentityProvider = MockIdentityProvider(
            mockListDevicesOutput: { _ in
                throw AWSCognitoIdentityProvider.InvalidUserPoolConfigurationException(
                    message: "invalid user poo configuration"
                )
            }
        )
        do {
            _ = try await plugin.fetchDevices()
            XCTFail("Should return an error if the result from service is invalid")
        } catch {
            guard case AuthError.configuration = error else {
                XCTFail("Should produce configuration error instead of \(error)")
                return
            }
        }
    }

    /// Test a fetchDevices call with NotAuthorizedException response from service
    ///
    /// - Given: an auth plugin with mocked service. Mocked service should mock a
    ///   NotAuthorizedException response
    ///
    /// - When:
    ///    - I invoke fetchDevices
    /// - Then:
    ///    - I should get a .notAuthorized error
    ///
    func testListDevicesWithNotAuthorizedException() async throws {

        mockIdentityProvider = MockIdentityProvider(
            mockListDevicesOutput: { _ in
                throw AWSCognitoIdentityProvider.NotAuthorizedException(
                    message: "not authorized"
                )
            }
        )
        do {
            _ = try await plugin.fetchDevices()
            XCTFail("Should return an error if the result from service is invalid")
        } catch {
            guard case AuthError.notAuthorized = error else {
                XCTFail("Should produce notAuthorized error instead of \(error)")
                return
            }
        }
    }

    /// Test a fetchDevices call with PasswordResetRequiredException response from service
    ///
    /// - Given: an auth plugin with mocked service. Mocked service should mock a
    ///   PasswordResetRequiredException response
    ///
    /// - When:
    ///    - I invoke fetchDevices
    /// - Then:
    ///    - I should get a .service error with .passwordResetRequired as underlyingError
    ///
    func testListDevicesWithPasswordResetRequiredException() async throws {

        mockIdentityProvider = MockIdentityProvider(
            mockListDevicesOutput: { _ in
                throw AWSCognitoIdentityProvider.PasswordResetRequiredException(
                    message: "password reset required"
                )
            }
        )
        do {
            _ = try await plugin.fetchDevices()
            XCTFail("Should return an error if the result from service is invalid")
        } catch {
            guard case AuthError.service(_, _, let underlyingError) = error else {
                XCTFail("Should produce service error instead of \(error)")
                return
            }
            guard case .passwordResetRequired = (underlyingError as? AWSCognitoAuthError) else {
                XCTFail("Underlying error should be passwordResetRequired \(error)")
                return
            }
        }
    }

    /// Test a fetchDevices call with ResourceNotFound response from service
    ///
    /// - Given: an auth plugin with mocked service. Mocked service should mock a
    ///   ResourceNotFoundException response
    ///
    /// - When:
    ///    - I invoke fetchDevices
    /// - Then:
    ///    - I should get a .service error with .resourceNotFound as underlyingError
    ///
    func testListDevicesWithResourceNotFoundException() async throws {

        mockIdentityProvider = MockIdentityProvider(
            mockListDevicesOutput: { _ in
                throw AWSCognitoIdentityProvider.ResourceNotFoundException(
                    message: "resource not found"
                )
            }
        )
        do {
            _ = try await plugin.fetchDevices()
            XCTFail("Should return an error if the result from service is invalid")
        } catch {
            guard case AuthError.service(_, _, let underlyingError) = error else {
                XCTFail("Should produce service error instead of \(error)")
                return
            }
            guard case .resourceNotFound = (underlyingError as? AWSCognitoAuthError) else {
                XCTFail("Underlying error should be resourceNotFound \(error)")
                return
            }
        }
    }

    /// Test a fetchDevices call with TooManyRequestsException response from service
    ///
    /// - Given: an auth plugin with mocked service. Mocked service should mock a
    ///   TooManyRequestsException response
    ///
    /// - When:
    ///    - I invoke fetchDevices
    /// - Then:
    ///    - I should get a .service error with .requestLimitExceeded as underlyingError
    ///
    func testListDevicesWithTooManyRequestsException() async throws {

        mockIdentityProvider = MockIdentityProvider(
            mockListDevicesOutput: { _ in
                throw AWSCognitoIdentityProvider.TooManyRequestsException(
                    message: "too many requests"
                )
            }
        )
        do {
            _ = try await plugin.fetchDevices()
            XCTFail("Should return an error if the result from service is invalid")
        } catch {
            guard case AuthError.service(_, _, let underlyingError) = error else {
                XCTFail("Should produce service error instead of \(error)")
                return
            }
            guard case .requestLimitExceeded = (underlyingError as? AWSCognitoAuthError) else {
                XCTFail("Underlying error should be requestLimitExceeded \(error)")
                return
            }
        }
    }

    /// Test a fetchDevices call with UserNotFound response from service
    ///
    /// - Given: an auth plugin with mocked service. Mocked service should mock a
    ///   UserNotConfirmedException response
    ///
    /// - When:
    ///    - I invoke fetchDevices
    /// - Then:
    ///    - I should get a .service error with .userNotConfirmed as underlyingError
    ///
    func testListDevicesWithUserNotConfirmedException() async throws {

        mockIdentityProvider = MockIdentityProvider(
            mockListDevicesOutput: { _ in
                throw AWSCognitoIdentityProvider.UserNotConfirmedException(
                    message: "user not confirmed"
                )
            }
        )
        do {
            _ = try await plugin.fetchDevices()
            XCTFail("Should return an error if the result from service is invalid")
        } catch {
            guard case AuthError.service(_, _, let underlyingError) = error else {
                XCTFail("Should produce service error instead of \(error)")
                return
            }
            guard case .userNotConfirmed = (underlyingError as? AWSCognitoAuthError) else {
                XCTFail("Underlying error should be userNotFound \(error)")
                return
            }
        }
    }

    /// Test a fetchDevices call with UserNotFound response from service
    ///
    /// - Given: an auth plugin with mocked service. Mocked service should mock a
    ///   UserNotFoundException response
    ///
    /// - When:
    ///    - I invoke fetchDevices
    /// - Then:
    ///    - I should get a .service error with .userNotFound as underlyingError
    ///
    func testListDevicesWithUserNotFoundException() async throws {

        mockIdentityProvider = MockIdentityProvider(
            mockListDevicesOutput: { _ in
                throw AWSCognitoIdentityProvider.UserNotFoundException(
                    message: "user not found"
                )
            }
        )
        do {
            _ = try await plugin.fetchDevices()
            XCTFail("Should return an error if the result from service is invalid")
        } catch {
            guard case AuthError.service(_, _, let underlyingError) = error else {
                XCTFail("Should produce service error instead of \(error)")
                return
            }
            guard case .userNotFound = (underlyingError as? AWSCognitoAuthError) else {
                XCTFail("Underlying error should be userNotFound \(error)")
                return
            }
        }
    }

}
